﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace DecisionTree
{
    class Forest
    {
        private byte[][] inputs;
        private byte[] outputs;

        public byte[][] testInputs;
        public byte[] testOutputs;

        private int[][] indexes;

        public int TrainError { get; set; }
        public int TestError { get; set; }

        public int NumTree { get; private set; }
        public int NumRecordsEachTree { get; private set; }

        public LightDecisionTree[] trees { get; private set; }

        public Random rnd = new Random();
        
        public int[][] RandomSubset(int numTree, int numRecordsEachTree)
        {
            this.NumTree = numTree;

            int total = Parameters.TotalRecordsTraining;

            int[][] rVal = new int[numTree][];

            for (int i = 0; i < numTree; i++)
            {
                rVal[i] = new int[numRecordsEachTree];
                for (int j = 0; j < numRecordsEachTree; j++)
                    rVal[i][j] = rnd.Next(total);
            }

            return rVal;
        }

        public Forest(int numTree, int numRecordsEachTree)
        {
            this.NumTree = numTree;
            this.NumRecordsEachTree = numRecordsEachTree;

            this.indexes = RandomSubset(numTree, numRecordsEachTree);

            this.inputs = FileOperations.ReadImagesBinary(Parameters.TrainInputPath);
            this.outputs = FileOperations.ReadLabels(Parameters.TrainOutputPath);

            this.testInputs = FileOperations.ReadImagesBinary(Parameters.TestInputPath);
            this.testOutputs = FileOperations.ReadLabels(Parameters.TestOutputPath);

            this.trees = new LightDecisionTree[numTree];

            for (int i = 0; i < this.NumTree; i++)
            {
                Console.WriteLine(i.ToString() + "/" + this.NumTree.ToString());
                
                this.trees[i] = new DecisionTree(this.inputs, this.outputs, Parameters.NumOutputClasses, i, this.indexes[i], this).ToLightDecisionTree();
            }            

            byte[][] testInputs = FileOperations.ReadImagesBinary(Parameters.TestInputPath);
            byte[] testOutputs = FileOperations.ReadLabels(Parameters.TestOutputPath);

            this.TrainError = this.Test(this.inputs, this.outputs);
            this.TestError = this.Test(testInputs, testOutputs);
        }

        private int ComputeWithHistogram(byte[] record)
        {
            double[] results = new double[Parameters.NumOutputClasses];

            for (int i = 0; i < trees.Length; i++)
            {
                double[] result = trees[i].ComputeWithHistogramToleranced(record);
                results.AddTo(result);
            }

            int majority = results.MaxIndex();
            //int majority = results.MinIndex();

            return majority;
        }

        private int Compute(byte[] record)
        {
            byte[] results = new byte[Parameters.NumOutputClasses];

            for (int i = 0; i < trees.Length; i++)
            {
                int result = trees[i].Compute(record);
                results[result]++;
            }

            int majority = results.MaxIndex();

            return majority;
        }

        public int Test(byte[][] inputs, byte[] outputs)
        {
            int miss = 0;

            for (int i = 0; i < inputs.Length; i++)
            {
                int result = ComputeWithHistogram(inputs[i]);
                if (result != outputs[i])
                    miss++;
            }

            return miss;
        }

        public void Log(string path)
        {
            using (StreamWriter writer = File.CreateText(path))
            {
                double trainAccuracy = (Convert.ToDouble(this.inputs.Length - this.TrainError) / this.inputs.Length) * 100.0;
                double testAccuracy = (Convert.ToDouble(this.testInputs.Length - this.TestError) / this.testInputs.Length) * 100.0;

                writer.WriteLine("Date: " + DateTime.Now.ToString());
                writer.WriteLine("Forest number of trees: " + this.NumTree);
                writer.WriteLine("Forest number of records each tree: " + this.NumRecordsEachTree);
                writer.WriteLine("Forest train error: " + this.TrainError + "( " + trainAccuracy + "% Accuracy )");
                writer.WriteLine("Forest test error: " + this.TestError + "( " + testAccuracy + "% Accuracy )");
                writer.WriteLine();

                writer.WriteLine(Parameters.Log());

                for (int i = 0; i < this.NumTree; i++)
                {
                    writer.WriteLine(this.trees[i].LogString);
                    writer.WriteLine();
                }
            }
        }
    }
}
